/**
 * axImgAreaSelect jQuery Plugin
 * version 0.0.1
 *
 * Uses imgAreaSelect by Michal Wojciechowshi (odyniec.net)
 *
 * Dual licensed under the MIT (MIT-LICENSE.txt)
 * and GPL (GPL-LICENSE.txt) licenses.
 *
 * http://odyniec.net/projects/imgareaselect
 *
 * Adds slight modifications to the original mentioned project
 **/

$(document).ready(function () {
    //Content URL path is relative if prototype is hosted on Axshare...
    var contentUrlPrefix = '';
    //Else if a local prototype, make content path absolute.
    if ($axure.player.settings.isAxshare === false) {
        contentUrlPrefix = window.AXSHARE_HOST_URL || 'http://share.axure.com';
    }

    $('<link>').appendTo('head').attr({
        rel: 'stylesheet',
        type: 'text/css',
        href: contentUrlPrefix + '/Content/plugins/axImgAreaSelect/styles/axImgAreaSelect.css'
    });
});

(function($) {
  // Math shortcuts
  var abs = Math.abs;
  var max = Math.max;
  var min = Math.min;
  var round = Math.round;

  /**
   * Creates a new div element via jQuery
   *
   * @return jQuery object representing a complete <div></div> element
   **/
  function div() {
    return $('<div/>');
  }

  /**
   * axImgAreaSelect intialization
   *
   * @param img - A HTML image element to attach the plugin to
   * @param options - options object describing nature of plugin
   **/
  $.axImgAreaSelect = function (img, options) {
    var $img = $(img);  // jQuery  object representing the image
    var isImgLoaded;    // Has the image finished loading?

    // ############################ PLUGIN ELEMENTS ###########################

    var $box = div();                                       // Container Box
    var $area = div();                                      // Selection Area
    var $border = div().add(div()).add(div()).add(div());   // Container Box Border
    var $outer = div().add(div()).add(div()).add(div());    // Area outside of Container Box
    var $handles = $([]);                                   // Resizing handles

    // Workaround for use with Opera. Additional div for switching between cursors
    var $areaOpera;

    // Image position (relative to viewport)
    var left;
    var top;

    // Image offset (as returned by .offset())
    var imgOffset = { left: 0, top: 0 };

    // Image dimentions (as returned by .width() and .height())
    var imgWidth;
    var imgHeight;

    // jQuery object representing parent element the plugin is appended to
    var $parent;

    // Parent offset (as returned by .offset())
    var parentOffset = { left: 0, top: 0 };

    // Base z-index for plugin elements
    var zIndex = 0;

    // Plugin elements positioning type
    var position = 'absolute';

    // Coordinates of the start point for move/resize operations
    var startX;
    var startY;

    // Horizontal and Vertical Scaling Factors
    var scaleX;
    var scaleY;

    // Current resize mode/direction ('nw', 'se', etc.)
    var resize;

    // Selection area constraints
    var minWidth;
    var minHeight;
    var maxWidth;
    var maxHeight;

    // Aspect ratio to maintain (floating point number)
    var aspectRatio;

    // Are the plugin elements currently displayed?
    var isShown;

    // Current selection (relative to parent element, aka container of image)
    var x1, y1, x2, y2;

    // Current selection (relative to scaled image, aka image)
    var selection = { x1: 0, y1: 0, x2: 0, y2: 0, width: 0, height: 0 };

    var docElem = document.documentElement;

    var userAgent = navigator.userAgent;

    var $p, d, i, o, w, h, adjusted;    // Various helper variables

    // ########## Selection Coords to Viewport Coords #########################

    /**
     * Translates selection X to viewport X
     * (Makes coords relative to image)
     *
     * @param x - Selection X
     * @return Viewport X
     **/
    function viewX(x) {
      return x + imgOffset.left - parentOffset.left;
    }

    /**
     * Translates selection Y to viewport Y
     * (Makes coords relative to image)
     *
     * @param Y - Selection Y
     * @return Viewport Y
     **/
    function viewY(y) {
      return y + imgOffset.top - parentOffset.top;
    }

    // ########### Viewport Coords to Selection Coords ########################

    /**
     * Translates viewport X to selection X
     * (Makes coords relative to parent)
     *
     * @param X - Viewport X
     * @return Selection X
     **/
    function selX(x) {
      return x - imgOffset.left + parentOffset.left;
    }

    /**
     * Translates viewport Y to selectionYX
     * (Makes coords relative to parent)
     *
     * @param Y - Viewport Y
     * @return Selection Y
     **/
    function selY(y) {
      return y - imgOffset.top + parentOffset.top;
    }

    // ############ Event coords to Viewport coords ###########################

    /**
     * Get event X and translate it to Viewport X
     *
     * @param event - Event object called
     * @return Viewport X
     **/
    function evX(event) {
      return event.pageX - parentOffset.left;
    }

    /**
     * Get event Y and translate it to Viewport Y
     *
     * @param event - Event object called
     * @return Viewport Y
     **/
    function evY(event) {
      return event.pageY - parentOffset.top;
    }

    // ############### Core Functionality #####################################

    /**
     * Gets the current area selection
     *
     * @param noScale - If set to <code>true</code>, scaling is not applied to
     *    the returned selection
     * @return Selection object
     **/
    function getSelection(noScale) {
      var sx = noScale || scaleX;
      var sy = noScale || scaleY;

      return {
        x1: round(selection.x1 * sx),
        y1: round(selection.y1 * sy),
        x2: round(selection.x2 * sx),
        y2: round(selection.y2 * sy),
        width: round(selection.x2 * sx) - round(selection.x1 * sx),
        height: round(selection.y2 * sy) - round(selection.y1 * sy)
      };
    }

    /**
     * Set the current area selection
     *
     * @param x1 - Upper left X coordinate of selection area
     * @param y1 - Upper left Y coordinate of selection area
     * @param x2 - Lower right X coordinate of selection area
     * @param y2 - Lower right Y coordinate of selection area
     * @param noScale - If set to <code>true</code>, scaling is not appplied to
     *    the new selection
     **/
    function setSelection(x1, y1, x2, y2, noScale) {
      var sx = noScale || scaleX;
      var sy = noScale || scaleY;

      selection = {
        x1: round(x1 / sx || 0),
        y1: round(y1 / sy || 0),
        x2: round(x2 / sx || 0),
        y2: round(y2 / sy || 0)
      };

      selection.width = selection.x2 - selection.x1;
      selection.height = selection.y2 - selection.y1;
    }

    /**
     * Recalculate image and parent offsets
     **/
    function adjust() {
      // Do not adjust if image is not loaded or if width is not a positive number.
      if (!isImgLoaded || !$img.width()) { return; }

      // Get current image offset. Rounded because values are floats
      imgOffset = { left: round($img.offset().left), top: round($img.offset().top) };

      // Get current image dimensions
      imgWidth = $img.innerWidth();
      imgHeight = $img.innerHeight();

      imgOffset.left += ($img.outerWidth() - imgWidth) >> 1;
      imgOffset.top += ($img.outerHeight() - imgHeight) >> 1;

      // Set boundaries for selection area
      minWidth = round(options.minWidth / scaleX) || 0;
      minHeight = round(options.minHeight / scaleY) || 0;
      maxWidth = round(min(options.maxWidth / scaleX || 1<<24, imgWidth));
      maxHeight = round(min(options.maxHeight / scaleY || 1<<24, imgHeight));

      // Workaround for jQuery 1.3.2 with incorrect offset calculations
      if ($().jquery == '1.3.2' && position == 'fixed'
          && !docElem['getBoundingClientRect']) {
        imgOffset.top += max(document.body.scrollTop, docElem.scrollTop);
        imgOffset.left += max(document.body.scrollLeft, docElem.scrollLeft);
      }

      // Determine current parent element offset
      parentOffset = /absolute|relative/.test($parent.css('position')) ?
        {
          left: round($parent.offset().left) - $parent.scrollLeft(),
          top: round($parent.offset().top) - $parent.scrollTop()
        } : position == 'fixed' ?
          {
            left: $(document).scrollLeft(),
            top: $(document).scrollTop()
          } : {
            left: 0,
            top: 0
          }

      left = viewX(0);
      top = viewY(0);

      // Check if current selection area is within current image boundaries.
      if (selection.x2 > imgWidth || selection.y2 > imgHeight) { doResize(); }
    }

    /**
     * Update plugin elements
     *
     * @param resetKeyPress - If set to <code>false</code>, this instance's
     *    keypress event handler is not activated
     **/
    function update(resetKeyPress) {
      // Do nothing if plugin is hidden
      if (!isShown) { return; }

      // Set position and size of container box and it's selection area
      $box.css({ left: viewX(selection.x1), top: viewY(selection.y1) })
        .add($area).width(w = selection.width).height(h = selection.height);

      // Reset position of selection area, borders and handles
      // (IE6/IE7 positions them incorrectly if this isn't done)
      $area.add($border).add($handles).css({ left: 0, top: 0 });

      // Set border dimensions
      $border
        .width(max(w - $border.outerWidth() + $border.innerWidth(), 0))
        .height(max(h - $border.outerHeight() + $border.innerHeight(), 0));

      // Arrange outer area elements (the darkened area)
      $($outer[0]).css({ left: left, top: top,
          width: selection.x1, height: imgHeight });
      $($outer[1]).css({ left: left + selection.x1, top: top,
          width: w, height: selection.y1 });
      $($outer[2]).css({ left: left + selection.x2, top: top,
          width: imgWidth - selection.x2, height: imgHeight });
      $($outer[3]).css({ left: left + selection.x1, top: top + selection.y2,
          width: w, height: imgHeight - selection.y2 });

      w -= $handles.outerWidth();
      h -= $handles.outerHeight();

      // Arrange Handles
      switch ($handles.length) {
        case 8:
          $($handles[4]).css({ left: w >> 1 });
          $($handles[5]).css({ left: w, top: h >> 1 });
          $($handles[6]).css({ left: w >> 1, top: h });
          $($handles[7]).css({ top: h >> 1 });
        case 4:
          $handles.slice(1,3).css({ left: w });
          $handles.slice(2,4).css({ top: h });
      }

      if (resetKeyPress !== false) {
        // Need to reset document keypress event handler: unbind current handler
        if ($.axImgAreaSelect.onKeyPress != docKeyPress) {
          $(document).unbind($.axImgAreaSelect.keyPress, $.axImgAreaSelect.onKeyPress);
        }

        if (options.keys) {
          // Set the document keypress event handler to this instance's docKeyPress func
          $(document)[$.axImgAreaSelect.keyPress]($.axImgAreaSelect.onKeyPress = docKeyPress);
        }
      }

      /*
       * Internet Explorer displays 1px-wide dashed borders incorrectly by
       * filling the spaces between dashes with white. Toggling the margin
       * property between 0 and "auto" fixes this in IE6 and IE7 (IE8 is still
       * broken). This workaround is not perfect, as it requires setTimeout()
       * and thus causes the border to flicker a bit, but I haven't found a
       * better solution.
       *
       * Note: This only happens with CSS borders, set with the borderWidth,
       * borderOpacity, borderColor1, and borderColor2 options (which are now
       * deprecated). Borders created with GIF background images are fine.
       */
      if (msie && $border.outerWidth() - $border.innerWidth() == 2) {
        $border.css('margin', 0);
        setTimeout(function() { $border.css('margin', 'auto'); }, 0);
      }
    }

    /**
     * Do the complete update sequence:
     *    1. Recalculate offsets (adujust())
     *    2. Update the elements (update(resetKeyPress))
     *    3. Set the correct values of x1, y1, x2, and y2
     *
     * @param resetKeyPress - If set to <code>false</code>, this instance's
     *    keypress event handler is not activated
     **/
    function doUpdate(resetKeyPress) {
      adjust();
      update(resetKeyPress);
      x1 = viewX(selection.x1);
      y1 = viewY(selection.y1);
      x2 = viewX(selection.x2);
      y2 = viewY(selection.y2);
    }

    /**
     * Hide or fade out an element (or multiple elements)
     *
     * @param $elem - A jQuery object containing the element(s) to hide/fade out
     * @param fn - Callback function to be called when fadeOut() completes
     **/
    function hide($elem, fn) {
      options.fadeSpeed ? $elem.fadeOut(options.fadeSpeed, fn) : $elem.hide();
    }

    /**
     * Adjust the x2/y2 coordinates to maintain aspect ratio (if defined)
     **/
    function fixAspectRatio(xFirst) {
      if (aspectRatio) {
        if (xFirst) {
          x2 = max(left, min(left + imgWidth, x1 + abs(y2 - y1) * aspectRatio * (x2 > x1 || -1)));
          y2 = round(max(top, min(top + imgHeight, y1 + abs(x2 - x1) / aspectRatio * (y2 > y1 || -1))));
          x2 = round(x2);
        } else {
          y2 = max(top, min(top + imgHeight, y1 + abs(x2 - x1) / aspectRatio * (y2 > y1 || -1)));
          x2 = round(max(left, min(left + imgWidth, x1 + abs(y2 - y1) * aspectRatio * (x2 > x1 || -1))));
          y2 = round(y2);
        }
      }
    }

    /**
     * Resize the selection area respecting the min/max dimensions and aspect ratio
     **/
    function doResize() {
      // Make sure the top left corner of selection stays within image bounds.
      // (might not be if the img src was dynamically changed)
      x1 = min(x1, left + imgWidth);
      y1 = min(y1, top + imgHeight);

      // Selection width is smaller than minWidth
      if (abs(x2 - x1) < minWidth) {
        x2 = x1 - minWidth * (x2 < x1 || -1);

        if (x2 < left) {
          x1 = left + minWidth;
        } else if (x2 > left + imgWidth) {
          x1 = left + imgWidth - minWidth;
        }
      }

      // Selection height is smaller than minHeight
      if (abs(y2 - y1) < minHeight) {
        y2 = y1 - minHeight * (y2 < y1 || -1);

        if (y2 < top) {
          y1 = top + minHeight;
        } else if (y2 > top + imgHeight) {
          y1 = top + imgHeight - minHeight;
        }
      }

      x2 = max(left, min(x2, left + imgWidth));
      y2 = max(top, min(y2, top + imgHeight));

      fixAspectRatio(abs(x2 - x1) < abs(y2 - y1) * aspectRatio);

      // Selection width is greater than maxwidth
      if (abs(x2 - x1) > maxWidth) {
        x2 = x1 - maxWidth * (x2 < x1 || -1);
        fixAspectRatio();
      }

      // Selection height is greater than maxHeight
      if (abs(y2 - y1) > maxHeight) {
        y2 = y1 - maxHeight * (y2 < y1 || -1);
        fixAspectRatio(true);
      }

      selection = {
        x1: selX(min(x1, x2)),
        y1: selY(min(y1, y2)),
        x2: selX(max(x1, x2)),
        y2: selY(max(y1, y2)),
        width: abs(x2 - x1),
        height: abs(y2 - y1)
      };

      update();

      options.onSelectChange(img, getSelection());
    }

    /**
     * Move the selection area
     *
     * @param newX1 - New Viewport X1
     * @param newY1 - New Viewport Y1
     **/
    function doMove(newX1, newY1) {
      x2 = (x1 = newX1) + selection.width;
      y2 = (y1 = newY1) + selection.height;

      $.extend(selection, {
        x1: selX(x1),
        y1: selY(y1),
        x2: selX(x2),
        y2: selY(y2)
      });

      update();

      options.onSelectChange(img, getSelection());
    }

    /**
     * Start Selection
     **/
    function startSelection() {
      $(document).unbind('mousemove', startSelection);
      adjust();

      x2 = x1;
      y2 = y1;
      doResize();

      resize = '';

      // Show plugin elements
      if (!$outer.is(':visible')) {
        $box.add($outer).hide().fadeIn(options.fadeSpeed || 0);
      }

      (function wait() {
        if ($('.aximgareaselect-outer').height() != 0) {
          options.onOuterOverlayShown(img, getSelection());
        } else {
          setTimeout(wait, 1);
        }
      })();

      isShown = true;

      $(document).unbind('mouseup', cancelSelection).mousemove(selectingMouseMove).one('mouseup', docMouseUp);
      $box.unbind('mousemove', areaMouseMove);

      options.onSelectStart(img, getSelection());
    }

    /**
     * Cancel Selection
     **/
    function cancelSelection() {
      $(document).unbind('mousemove', startSelection).unbind('mouseup', cancelSelection);
      hide($box.add($outer));

      setSelection(selX(x1), selY(y1), selX(x1), selY(y1));

      // If this is an API call, callback functions should not be called
      if (!(this instanceof $.axImgAreaSelect)) {
        options.onSelectChange(img, getSelection());
        options.onSelectEnd(img, getSelection());
        options.onSelectCancel(img, getSelection());
      }
    }

    /**
     * Apply style options to plugin element (or multiple elements)
     *
     * @param $elem - jQuery object representing element9s0 to style
     * @param props - Object that maps option names to corresponding CSS props
     **/
    function styleOptions($elem, props) {
      for (var option in props) {
        if (options[option] !== undefined) {
          $elem.css(props[option], options[option]);
        }
      }
    }

    /**
     * Set plugin options
     *
     * @param newOptions - The new set of options object
     **/
    function setOptions(newOptions) {
      if (newOptions.parent) {
        ($parent = $(newOptions.parent)).append($box.add($outer));
      }

      // Merge new options with existing ones
      $.extend(options, newOptions);

      adjust;

      if (newOptions.handles != null) {
        // Rebuild selection area handles
        $handles.remove();
        $handles = $([]);

        i = newOptions.handles ? newOptions.handles == 'corners' ? 4 : 8 : 0;

        while (i--) {
          $handles = $handles.add(div());
        }

        // Add a class to handles and set the CSS properties
        $handles.addClass(options.classPrefix + '-handle').css({
          position: 'absolute',
          fontSize: 0,      // Needs to be set to zero or IE makes handles too large
          zIndex: zIndex + 1 || 1
        });

        // If handle width/height has not been set with CSS rules, set to default 5px
        if (!parseInt($handles.css('width')) >= 0) {
          $handles.width(5).height(5);
        }

        // If the borderWidth options is in use, add a solid border to handles
        if (o = options.borderWidth) {
          $handles.css({ borderWidth: o, borderStyle: 'solid' });
        }

        // Add other style options to handles
        styleOptions($handles, {
          borderColor1: 'border-color',
          borderColor2: 'background-color',
          borderOpacity: 'opacity'
        });
      }

      // Calculate scale factors
      scaleX = options.imageWidth / imgWidth || 1;
      scaleY = options.imageHeight / imgHeight || 1;

      // Set selection
      if (newOptions.x1 != null) {
        setSelection(newOptions.x1, newOptions.y1, newOptions.x2, newOptions.y2);
        newOptions.show = !newOptions.hide();
      }

      // Enable keyboard support
      if (newOptions.keys) {
        options.keys = $.extend({ shift: 1, ctrl: 'resize' }, newOptions.keys);
      }

      // Add classes to plugin components
      $outer.addClass(options.classPrefix + '-outer');
      $area.addClass(options.classPrefix + '-selection');

      for (i = 0; i < 4; i++) {
        $($border[i]).addClass(options.classPrefix + '-border' + (i+1));
      }

      // Apply style options
      styleOptions($area, { selectionColor: 'background-color',
            selectionOpacity: 'opacity' });
      styleOptions($border, { borderOpacity: 'opacity',
          borderWidth: 'border-width' });
      styleOptions($outer, { outerColor: 'background-color',
          outerOpacity: 'opacity' });
      if (o = options.borderColor1) {
          $($border[0]).css({ borderStyle: 'solid', borderColor: o });
      }
      if (o = options.borderColor2) {
          $($border[1]).css({ borderStyle: 'dashed', borderColor: o });
      }

      // Append all selection area elements to the container box
      $box.append($area.add($border).add($areaOpera)).append($handles);

      if (msie) {
        if (o = ($outer.css('filter')||'').match(/opacity=(\d+)/)) {
          $outer.css('opacity', o[1]/100);
        }
        if (o = ($border.css('filter')||'').match(/opacity=(\d+)/)) {
          $border.css('opacity', o[1]/100);
        }
      }

      if (newOptions.hide) {
        hide($box.add($outer));
      } else if (newOptions.show && isImgLoaded) {
        isShown = true;
        $box.add($outer).fadeIn(options.fadeSpeed || 0);
        doUpdate();
      }

      // Calculate the aspect ratio factor
      aspectRatio = (d = (options.aspectRatio || '').split(/:/))[0] / d[1];

      $img.add($outer).unbind('mousedown', imgMouseDown);

      if (options.disable || options.enable === false) {
          // Disable plugin
          $box.unbind('mousemove', areaMouseMove).unbind('mousedown', areaMouseDown);
          $(window).unbind('resize', windowResize);
      } else {
        // Enable plugin
        if (options.enable || options.disable === false) {
          if (options.resizable || options.movable) {
            $box.mousemove(areaMouseMove).mousedown(areaMouseDown);
          }

          $(window).resize(windowResize);
        }

        if (!options.persistent) {
          $img.add($outer).mousedown(imgMouseDown);
        }
      }

      options.enable = options.disable = undefined;
    }

    /**
     * Remove plugin completely
     **/
    this.remove = function() {
      // Call setOptions with { disable: true } to unbind all event handlers
      setOptions({ disable: true });
      $box.add($outer).remove();
    }

    /**
     * Returns a dataURL of the image capatured within the selected area
     *
     * @return dataURL object representing the cropped image
     **/
    function cropImage() {
      var bounds = getSelection();
      var cropCanvas = document.getElementById('tempCropCanvas');

      if (!cropCanvas) {
        var cropCanvas = document.createElement('canvas');
        document.body.appendChild(cropCanvas);
      }

      cropCanvas.setAttribute('id', 'tempCropCanvas');
      cropCanvas.setAttribute('hidden', true);
      cropCanvas.setAttribute('width', bounds.width);
      cropCanvas.setAttribute('height', bounds.height);

      var cropContext = cropCanvas.getContext("2d");

      cropContext.drawImage(img, bounds.x1, bounds.y1, bounds.width, bounds.height, 0, 0, bounds.width, bounds.height);

      return (cropCanvas.toDataURL("image/png", 1.0));
    }


    // ################### Event Handlers #####################################

    /**
     * Image load event handler. This is the final part of the initialization process.
     **/
    function imgLoad() {
      isImgLoaded = true;

      // Set Default Options
      setOptions(options = $.extend({
        classPrefix: 'aximgareaselect',
        movable: true,
        parent: 'body',
        resizable: true,
        resizeMargin: 10,
        onInit: function () {},
        onOuterOverlayShown: function() {},
        onSelectStart: function() {},
        onSelectChange: function() {},
        onSelectEnd: function() {},
        onSelectCancel: function() {}
      }, options));

      $box.add($outer).css({ visibility: '' });

      if (options.show) {
        isShown = true;
        adjust();
        update();

        $box.add($outer).hide().fadeIn(options.fadeSpeed || 0);
      }

      /*
       * Call the onInit callback. The setTimeout() call is used to ensure
       * that the plugin has been fully initialized and the object instance is
       * available (so that it can be obtained in the callback).
       */
      setTimeout(function() { options.onInit(img, getSelection()); }, 0);
    }

    /**
     * Selection area mousemove event handler
     *
     * @param event - The event object
     **/
    function areaMouseMove(event) {
      var x = selX(evX(event)) - selection.x1;
      var y = selY(evY(event)) - selection.y1;

      if (!adjusted) {
        adjust();
        adjusted = true;

        $box.one('mouseout', function () { adjusted = false; });
      }

      // Clear resize mode
      resize = '';

      // Check if mouse pointer is over the resize margin area
      // and set resize mode accordingly
      if (options.resizable) {
        if (y <= options.resizeMargin) {
          resize = 'n';
        } else if (y >= selection.height - options.resizeMargin) {
          resize = 's';
        }

        if (x <= options.resizeMargin) {
          resize += 'w';
        } else if (x >= selection.width - options.resizeMargin) {
          resize += 'e';
        }
      }

      $box.css('cursor', resize ? resize + '-resize' : options.movable ? 'move' : '');

      if ($areaOpera) {
        $areaOpera.toggle();
      }
    }

    /**
     * Selection area mousedown event handler
     *
     * @param event - The event object
     * @return false
     **/
    function areaMouseDown(event) {
      if (event.which != 1) { return false; }

      adjust();

      if (resize) {
        $('body').css('cursor', resize + '-resize');

        x1 = viewX(selection[/w/.test(resize) ? 'x2' : 'x1']);
        y1 = viewY(selection[/n/.test(resize) ? 'y2' : 'y1']);

        $(document).mousemove(selectingMouseMove).one('mouseup', docMouseUp);
        $box.unbind('mousemove', areaMouseMove);
      } else if (options.movable) {
        startX = left + selection.x1 - evX(event);
        startY = top + selection.y1 - evY(event);

        $box.unbind('mousemove', areaMouseMove);

        $(document).mousemove(movingMouseMove).one('mouseup', function() {
          options.onSelectEnd(img, getSelection());

          $(document).unbind('mousemove', movingMouseMove);
          $box.mousemove(areaMouseMove);
        });
      } else {
        $img.mousedown(event);
      }

      return false;
    }

    /**
     * Mousemove event handler triggered when the user is selecting an area
     *
     * @param event - The event object
     * @return false
     **/
    function selectingMouseMove(event) {
      x2 = /w|e|^$/.test(resize) || aspectRatio ? evX(event) : viewX(selection.x2);
      y2 = /n|s|^$/.test(resize) || aspectRatio ? evY(event) : viewY(selection.y2);

      doResize();

      return false;
    }

    /**
     * Mousemove event handler triggered when the selection area is being moved
     *
     * @param event - The event object
     * @return false
     **/
    function movingMouseMove(event) {
      x1 = max(left, min(startX + evX(event), left + imgWidth - selection.width));
      y1 = max(top, min(startY + evY(event), top + imgHeight - selection.height));

      doMove(x1, y1);

      event.preventDefault();
      return false;
    }

    /**
     * Window resize event handler
     **/
    function windowResize() {
      doUpdate(false);
    }

    /**
     * Image mousedown event handler
     *
     * @param event - The event handler
     * @return false
     **/
    function imgMouseDown(event) {
      // Ignore event if animation is in progress
      if (event.which != 1 || $outer.is(':animated')) { return false; }

      adjust();
      startX = x1 = evX(event);
      startY = y1 = evY(event);

      // Selection will start when the mouse is moved
      $(document).mousemove(startSelection).mouseup(cancelSelection);

      return false;
    }

    /**
     * Document mouseup event handler
     *
     * @param event - The event object
     **/
    function docMouseUp(event) {
      // Set back the default cursor
      $('body').css('cursor', '');

      // If autoHide is enabled, or if the selection has zero width/height,
      // hide the selection and the outer area
      if (options.autoHide || selection.width * selection.height == 0) {
        hide($box.add($outer), function() { $(this).hide(); });
      }

      $(document).unbind('mousemove', selectingMouseMove);
      $box.mousemove(areaMouseMove);

      options.onSelectEnd(img, getSelection());
    }

    /**
     * Dccument keypress event handler
     *
     * @param event - The event object
     * @return false
     **/
    var docKeyPress = function(event) {
      var k = options.keys, d, t, key = event.keyCode;

      d = !isNaN(k.alt) && (event.altKey || event.originalEvent.altKey) ? k.alt :
          !isNaN(k.ctrl) && event.ctrlKey ? k.ctrl :
          !isNaN(k.shift) && event.shiftKey ? k.shift :
          !isNaN(k.arrows) ? k.arrows : 10;

      if (k.arrows == 'resize' || (k.shift == 'resize' && event.shiftKey) ||
          (k.ctrl == 'resize' && event.ctrlKey) ||
          (k.alt == 'resize' && (event.altKey || event.originalEvent.altKey)))
      {
          /* Resize selection */
          switch (key) {
            case 37:
                /* Left */
                d = -d;
            case 39:
                /* Right */
                t = max(x1, x2);
                x1 = min(x1, x2);
                x2 = max(t + d, x1);
                fixAspectRatio();
                break;
            case 38:
                /* Up */
                d = -d;
            case 40:
                /* Down */
                t = max(y1, y2);
                y1 = min(y1, y2);
                y2 = max(t + d, y1);
                fixAspectRatio(true);
                break;
            default:
                return;
          }

          doResize();
      }
      else {
          /* Move selection */

          x1 = min(x1, x2);
          y1 = min(y1, y2);

          switch (key) {
          case 37:
              /* Left */
              doMove(max(x1 - d, left), y1);
              break;
          case 38:
              /* Up */
              doMove(x1, max(y1 - d, top));
              break;
          case 39:
              /* Right */
              doMove(x1 + min(d, imgWidth - selX(x2)), y1);
              break;
          case 40:
              /* Down */
              doMove(x1, y1 + min(d, imgHeight - selY(y2)));
              break;
          default:
              return;
          }
      }

      return false;
    };

    // ##################### API Functions ####################################

    /**
     * Get current options
     * @return An object containing the set of options currently in use
     **/
    this.getOptions = function() { return options; };

    /**
     * Set plugin options
     * @param newOptions - The new options object
     **/
    this.setOptions = setOptions;

    /**
     * Get the current selection
     * @param noScale - If set to <code>true</code>, scaling is not applied to the
     *    returned selection
     **/
    this.getSelection = getSelection;

    /**
     * Set the current selection
     * @param x1 - X coord from upper left hand corner of selection area
     * @param y1 - Y coord from upper left hand corner of selection area
     * @param x2 - X coord from lower right hand corner of selection area
     * @param y2 - Y coord from lower right hand corner of selection area
     * @param noScale - If set to <code>true</code>, scaling is not applied to the
     *    returned selection
     **/
    this.setSelection = setSelection;

    /**
     * Cancel selection
     **/
    this.cancelSelection = cancelSelection;

    /**
     * Update plugin elements
     * @param resetKeyPress - If set to <code>false</code>, this instance's keypress
     *    event handler is not activated
     **/
    this.update = doUpdate;

    this.cropImage = cropImage;

    // ##################### Main Executing Function ##########################

    // Browser detection
    var msie = (/msie ([\w.]+)/i.exec(userAgent)||[])[1],
      opera = /opera/i.test(userAgent),
      safari = /webkit/i.test(userAgent) && !/chrome/i.test(userAgent);

    // Traverse the image's parent elements (up to the <body>) and find the
    // highest z-index
    $p = $img;

    while ($p.length) {
      zIndex = max(zIndex, !isNaN($p.css('z-index')) ? $p.css('z-index') : zIndex);

      // Check if any of ancestor elements has fixed position
      if ($p.css('position') == 'fixed') {
        position = 'fixed';
      }

      $p = $p.parent(':not(body)');
    }

    // If z-index is given as an option, it overrides the one found by above loop
    zIndex = options.zIndex || zIndex;

    if (msie) {
      $img.attr('unselectable', 'on');
    }

    // in IE and WebKit, we need to use the keydown event instead of keypress
    $.axImgAreaSelect.keyPress = msie || safari ? 'keydown' : 'keypress';

    /*
     * There is a bug affecting the CSS cursor property in Opera (observed in
     * versions up to 10.00) that prevents the cursor from being updated unless
     * the mouse leaves and enters the element again. To trigger the mouseover
     * event, we're adding an additional div to $box and we're going to toggle
     * it when mouse moves inside the selection area.
     */
    if (opera) {
      $areaOpera = div().css({
        width: '100%',
        height: '100%',
        position: 'absolute',
        zIndex: zIndex + 2 || 2
      });
    }

    /*
     * We initially set visibility to "hidden" as a workaround for a weird
     * behaviour observed in Google Chrome 1.0.154.53 (on Windows XP). Normally
     * we would just set display to "none", but, for some reason, if we do so
     * then Chrome refuses to later display the element with .show() or
     * .fadeIn().
     */
    $box.add($outer).css({
      visibility: 'hidden',
      position: position,
      overflow: 'hidden',
      zIndex: zIndex || '0'
    });

    $box.css({ zIndex: zIndex + 2 || 2 });
    $area.add($border).css({ position: 'absolute', fontSize: 0 });

    /*
     * If the image has been fully loaded, or if it is not really an image (eg.
     * a div), call imgLoad() immediately; otherwise, bind it to be called once
     * on image load event.
     */
    img.complete || img.readyState == 'complete' || !$img.is('img')
      ? imgLoad()
      : $img.one('load', imgLoad);

    /*
     * MSIE 9.0 doesn't always fire the image load event -- resetting the src
     * attribute seems to trigger it. The check is for version 7 and above to
     * accommodate for MSIE 9 running in compatibility mode.
     */
    if (!isImgLoaded && msie && msie >= 7) {
      img.src = img.src;
    }
  };

  /**
   * Invoke axImgAreaSelect on a jQuery object containing the image(s)
   *
   * @param options - Options object
   * @return jQuery object or a reference to axImgAreaSelect instances
   *    (if the <code>instance</code> option was specified)
   **/
  $.fn.axImgAreaSelect = function (options) {
    options = options || {};

    this.each(function() {
      // Is there already an axImgAreaSelect bound to this element?
      if ($(this).data('axImgAreaSelect')) {
        if (options.remove) {
          // Remove plugin
          $(this).data('axImgAreaSelect').remove();
          $(this).removeData('axImgAreaSelect');
        } else {
          // Reset options
          $(this).data('axImgAreaSelect').setOptions(options);
        }
      } else if (!options.remove) {

        if (options.enable === undefined && options.disable == undefined) {
          options.enable = true;
        }

        $(this).data('axImgAreaSelect', new $.axImgAreaSelect(this, options));
      }
    });

    if (options.instance) {
      // Return the axImgAreaSelect instance bound to the first element in the set
      return ($(this).data('axImgAreaSelect'));
    }

    return this;
  };

})(jQuery);
